RustをつかってAWS Lambdaを実装&AWS CDKでデプロイする
Introduction
最近Rustが各所で盛り上がっています。
Rustは5年連続で最も愛されているプログラミング言語になっている
開発者に人気のプログラミング言語です。
また、LinuxカーネルにRustを採用しようという動きがあったり、
AndroidのOS開発でRustをサポート 、といった具合に、
さまざまなところでRustの話題がでています。
AWS・Google・MicrosoftなどがRust Foundationを立ち上げたことも後押しとなり、
使えるようになりたいなーということで最近私もさわりはじめました。
本記事ではカスタムランタイムをつかってRustでAWS Lambdaを作成し、AWS CDKでデプロイしたり
Localstackを使ってローカルでLambdaを実行したりしてみます。
本記事は、ここにあるソースほぼそのまま参考にして作成しました。
実際は(私の環境では)このままだと動かなかったので、
これを参考にしてさらに内容を絞ったサンプルを作成しました。
作成したサンプルはここです。
Environment
- OS : MacOS 10.15.7
- Node : v14.4.0
- Docker : 20.10.5
- aws-cli : 2.1.38
- Rust : 1.51.0
※AWSアカウント設定などは終わっている前提
Create Rsut + Lambda example
ではRustで動くAWS Lambdaをつくっていきましょう。
Create & Setup Project
まずはcargoでプロジェクトを新規作成。
% cargo new rust-lambda-cdk Created binary (application) `rust-lambda-cdk` package
cargo.tomlに必要な情報を記述します。
エントリーポイントとなるプログラムはbootstrap.rsです。
[package] name = "rust-lambda-cdk" version = "0.1.0" authors = ["your name <your mail address>"] edition = "2018" readme = "README.md" license = "MIT" [lib] name = "lib" path = "src/lib.rs" [[bin]] name = "bootstrap" path = "src/bin/bootstrap.rs" [dependencies] lambda = { package = "netlify_lambda", version = "0.2.0" } tokio = "1.5.0" serde = "1.0.125" serde_derive = "1.0.125" serde_json = "1.0.64" [dev-dependencies] pretty_assertions = "0.7.2"
次にnpm設定。
% npm init ・・・
必要になるnpmモジュールをインストールします。
- @aws-cdk/aws-lambda
- @aws-cdk/core
- @aws-cdk/aws-s3
- @types/node
- aws-cdk
- ts-node
- typescript
- tsconfig-paths
% npm install --save @aws-cdk/aws-lambda ・・・・・・・
lambdaのデプロイはcdkを使用します。
今回cdkはTypeScriptで記述するので、tsconfigを作成しましょう。
% tsc --init
tsconfig.json は下のような感じで記述します。
{ "compilerOptions": { "target": "ES2018", "module": "commonjs", "lib": ["es2016", "es2017.object", "es2017.string"], "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "baseUrl": "." } }
次に、ローカルでLinuxを対象にしてビルドしなければいけないのでクロスコンパイラをインストールします。
% brew install filosottile/musl-cross/musl-cross
.cargo/configファイルを作成し、linkerの指定。
[target.x86_64-unknown-linux-musl] linker = "x86_64-linux-musl-gcc"
package.jsonのscriptsにビルドやデプロイ用スクリプトを記述します。
ビルドやデプロイなど、必要なコマンドを定義します。
・・・ "scripts": { "build": "rustup target add x86_64-unknown-linux-musl && cargo build --release --target x86_64-unknown-linux-musl && mkdir -p ./target/cdk/release && cp ./target/x86_64-unknown-linux-musl/release/bootstrap ./target/cdk/release/bootstrap", "build:clean": "rm -r ./target/cdk/release || echo '[build:clean] No existing release found.'", "deploy": "npm run build:clean && npm run build && npm run cdk:deploy", "cdk:deploy": "[[ $CI == 'true' ]] && export CDK_APPROVAL='never' || export CDK_APPROVAL='broadening'; cdk deploy --require-approval $CDK_APPROVAL '*'", "cdk:bootstrap": "cdk bootstrap aws://$(aws sts get-caller-identity | jq -r .Account)/$AWS_REGION", "cdklocal:start": "docker-compose up", "cdklocal:clear-cache": "(rm ~/.cdk/cache/accounts.json || true) && (rm ~/.cdk/cache/accounts_partitions.json || true)", "cdklocal:deploy": "npm run --silent cdklocal:clear-cache && CDK_LOCAL=true cdklocal deploy --require-approval never '*'", "cdklocal:bootstrap": "npm run --silent cdklocal:clear-cache && CDK_LOCAL=true cdklocal bootstrap aws://000000000000/us-west-1" }, ・・・
cdklocal:〜のコマンドは、LocalStackを使ってローカルでLambdaの動作確認をするためのコマンドです。
LocalStackはでDockerコンテナで起動するので、使用する場合には事前にDockerをインストールしておきます。
build Rust sources
環境ができたので、Rustのソースを記述します。
Lambdaのエントリーポイントとなるsrc/bin/bootstrap.rsを作成しましょう。
use lambda::handler_fn; use ::lib::handler; use ::lib::LambdaError; #[tokio::main] async fn main() -> Result<(), LambdaError> { println!("execute bootstrap#main"); let runtime_handler = handler_fn(handler); lambda::run(runtime_handler).await?; Ok(()) }
src/lib.rsではbootstrapから利用するハンドラを定義します。
nameという名前のパラメータをうけとったら、その値で文字列を生成してJsonで返します。
use lambda::Context; use serde_json::{json, Value}; pub type LambdaError = Box<dyn std::error::Error + Send + Sync + 'static>; pub async fn handler(event: Value, _: Context) -> Result<Value, LambdaError> { println!("execute lib#handler"); let name = event["name"].as_str().unwrap_or("world"); Ok(json!({ "message": format!("Hello, {}!", name) })) }
npm runでビルドしてみます。
% npm run build > rust-lambda-cdk@1.0.0 build /rust-lambda-cdk > rustup target add x86_64-unknown-linux-musl && cargo build --release --target x86_64-unknown-linux-musl && mkdir -p ./target/cdk/release && cp ./target/x86_64-unknown-linux-musl/release/bootstrap ./target/cdk/release/bootstrap info: component 'rust-std' for target 'x86_64-unknown-linux-musl' is up to date Finished release [optimized] target(s) in 0.52s
target/cdk/release以下にファイルが生成されます。
Deploy via CDK
ビルドが完了したので、次はCDKをつかってAWS Lambdaをデプロイします。
まずはcdk.jsonを作成。
{ "app": "ts-node -r tsconfig-paths/register deploy/stack.ts" }
deployディレクトリを作成し、そこにstack.tsを下記内容で記述します。
import * as cdk from "@aws-cdk/core"; import { LambdaStack } from "./lib/lambda-stack"; import * as pkg from "../package.json"; const { BENCHMARK_SUFFIX } = process.env; const STACK_NAME = BENCHMARK_SUFFIX ? `${pkg.name}-${BENCHMARK_SUFFIX}` : pkg.name; export default class Stack { public lambdaStack: LambdaStack; constructor(app: cdk.App) { this.lambdaStack = new LambdaStack(app, `${STACK_NAME}`, {}); } } const app = new cdk.App(); new Stack(app);
deploy/lib/lambda-stack.tsを作成します。
ここではLambdaの設定を行います。
idはcargoで指定しているpackage.name(rust-lambda-cdk)、
functionNameは${id} + "-main"となります。
(rust-lambda-cdk-main)
import * as core from "@aws-cdk/core"; import * as lambda from "@aws-cdk/aws-lambda"; import * as s3 from "@aws-cdk/aws-s3"; import * as cdk from "@aws-cdk/core"; const { CDK_LOCAL } = process.env; interface Props {} export class LambdaStack extends core.Stack { constructor(scope: cdk.App, id: string, props: Props) { super(scope, id); const bootstrapLocation = `${__dirname}/../../target/cdk/release`; const entryId = "main"; const entryFnName = `${id}-${entryId}`; const entry = new lambda.Function(this, entryId, { functionName: entryFnName, description: "Rust + Lambda + CDK", runtime: lambda.Runtime.PROVIDED_AL2, handler: `${id}`, code: CDK_LOCAL !== "true" ? lambda.Code.fromAsset(bootstrapLocation) : lambda.Code.fromBucket(s3.Bucket.fromBucketName(this, `LocalBucket`, "__local__"), bootstrapLocation), memorySize: 256, timeout: cdk.Duration.seconds(10), tracing: lambda.Tracing.ACTIVE, }); entry.addEnvironment("AWS_NODEJS_CONNECTION_REUSE_ENABLED", "1"); core.Aspects.of(entry).add(new cdk.Tag("service-type", "API")); core.Aspects.of(entry).add(new cdk.Tag("billing", `lambda-${entryFnName}`)); } }
CDK用のtsを記述したら、AWS_REGION環境変数をセットし、boostrapコマンドを実行します。
※ 事前にaws configureで適切なLambdaへのアクセス設定をしておく
% export AWS_REGION=<your target region> % npm run bootstrap > rust-lambda-cdk@1.0.0 cdk:bootstrap /Users/nakamurashuuta/dev/rust/rust-lambda-cdk > cdk bootstrap aws://$(aws sts get-caller-identity | jq -r .Account)/$AWS_REGION ⏳ Bootstrapping environment aws://xxxxxxxx/us-west-1... ✅ Environment aws://xxxxxxxx/us-west-1 bootstrapped (no changes).
成功したら次はビルドしたLambdaプログラムをデプロイ。
% npm run deploy > rust-lambda-cdk@1.0.0 deploy /dev/rust/rust-lambda-cdk > npm run build:clean && npm run build && npm run cdk:deploy > rust-lambda-cdk@1.0.0 build:clean /dev/rust/rust-lambda-cdk > rm -r ./target/cdk/release || echo '[build:clean] No existing release found.' > rust-lambda-cdk@1.0.0 build /dev/rust/rust-lambda-cdk > rustup target add x86_64-unknown-linux-musl && cargo build --release --target x86_64-unknown-linux-musl && mkdir -p ./target/cdk/release && cp ./target/x86_64-unknown-linux-musl/release/bootstrap ./target/cdk/release/bootstrap info: component 'rust-std' for target 'x86_64-unknown-linux-musl' is up to date Finished release [optimized] target(s) in 0.38s > rust-lambda-cdk@1.0.0 cdk:deploy /dev/rust/rust-lambda-cdk > [[ $CI == 'true' ]] && export CDK_APPROVAL='never' || export CDK_APPROVAL='broadening'; cdk deploy --require-approval $CDK_APPROVAL '*' rust-lambda-cdk: deploying... ✅ rust-lambda-cdk (no changes) Stack ARN: arn:aws:cloudformation:us-west-1:xxxxxxxxx:stack/rust-lambda-cdk/xxxx-xxxxx-xxxxx
invokeコマンドでデプロイしたLambdaを実行してみましょう。
% aws lambda invoke \ --function-name rust-lambda-cdk-main \ --cli-binary-format raw-in-base64-out \ --region $AWS_REGION \ --payload '{}' \ tmp-output.json > /dev/null && cat tmp-output.json && rm tmp-output.json {"message":"Hello, world!"}
Rustで記述したAWS LamdaがCDKでデプロイされ、実行できるのを確認しました。
LocalStackを使う
次は LocalStackを使って、ローカルでLamdaを動かしてみます。
dockerが使用できる状態になっていれば、docker-compose.ymlを用意するだけ。
内容はほぼここにあるようなものです。
version: "2.1" services: localstack: container_name: "${LOCALSTACK_DOCKER_NAME-localstack_main}" image: localstack/localstack:latest network_mode: bridge ports: - "4566:4566" - "4571:4571" - "${PORT_WEB_UI-9888}:${PORT_WEB_UI-9888}" environment: - SERVICES=${SERVICES-serverless,cloudformation,iam,sts,sqs,ssm,s3,acm,cloudwatch,cloudwatch-logs,lambda,apigateway} - DEFAULT_REGION=${DEFAULT_REGION-us-west-1} - DEBUG=${DEBUG- } - DATA_DIR=${DATA_DIR- } - PORT_WEB_UI=${PORT_WEB_UI-9888} - LAMBDA_EXECUTOR=${LAMBDA_EXECUTOR- } - LAMBDA_REMOTE_DOCKER=${LAMBDA_REMOTE_DOCKER-false} - KINESIS_ERROR_PROBABILITY=${KINESIS_ERROR_PROBABILITY- } - DOCKER_HOST=unix:///var/run/docker.sock - HOST_TMP_FOLDER=${TMPDIR:-/tmp/localstack} volumes: - "${TMPDIR:-/tmp/localstack}:/tmp/localstack" - "/var/run/docker.sock:/var/run/docker.sock"
cdklocal:startコマンド(docker-compose upしてるだけ)でLocalStackコンテナの起動をします。
% npm run cdklocal:start > rust-lambda-cdk@1.0.0 cdklocal:start /dev/rust/rust-lambda-cdk > docker-compose up Docker Compose is now in the Docker CLI, try `docker compose up` Creating localstack_main ... done Attaching to localstack_main
LocalStackに対してbootstrapとdeployを実行しましょう。
% npm run cdklocal:bootstrap > rust-lambda-cdk@1.0.0 cdklocal:bootstrap /dev/rust/rust-lambda-cdk > npm run --silent cdklocal:clear-cache && CDK_LOCAL=true cdklocal bootstrap aws://000000000000/us-west-1 ⏳ Bootstrapping environment aws://000000000000/us-west-1... CDKToolkit: creating CloudFormation changeset... 10:28:10 | UPDATE_IN_PROGRESS | AWS::CloudFormation::Stack | UsePublicAccessBlockConfiguration ✅ Environment aws://000000000000/us-west-1 bootstrapped. % npm run cdklocal:deploy > rust-lambda-cdk@1.0.0 cdklocal:deploy /dev/rust/rust-lambda-cdk > npm run --silent cdklocal:clear-cache && CDK_LOCAL=true cdklocal deploy --require-approval never '*' rust-lambda-cdk: deploying... rust-lambda-cdk: creating CloudFormation changeset... 10:29:05 | UPDATE_IN_PROGRESS | AWS::CloudFormation::Stack | CDKMetadata/Condition ✅ rust-lambda-cdk Stack ARN: arn:aws:cloudformation:us-west-1:000000000000:stack/rust-lambda-cdk/xxxxx
LocalStackに対してinvokeしてみます。(今度はパラメータつき)
% aws --endpoint-url=http://localhost:4566 lambda invoke \ --function-name rust-lambda-cdk-main \ --cli-binary-format raw-in-base64-out \ --payload '{"name": "taro"}' \ tmp-output.json > /dev/null && cat tmp-output.json && rm tmp-output.json {"message":"Hello, taro!"}
Summary
今回はRustでAWS Lambdaを実装してCDKでデプロイしてみました。
RustとLambdaは相性がいいといわれてますが、
開発環境も使いやすくなればさらに開発しやすくなりますね。